今天的完成畫面只有一行:Hello from hello.txt on VirtIO disk!。
不過,這行文字不能來自使用者程式裡的字串,也不能由核心直接印出預先寫好的答案。
它必須存在磁碟映像中,經過裝置驅動與檔案查找,再交到 U-mode 程式手上。
我們會把這條路徑拆成三個檢查點:先確認磁碟內容,再確認核心讀取,最後確認資料能安全返回使用者空間。
《Operating System Concepts》第 10 版第 12.2 節介紹核心如何與 I/O 裝置交換控制與資料,第 13.1 節則從檔案的角度描述具名資料集合。
兩者不是同一層:區塊裝置理解的是 sector 編號,應用程式關心的卻是檔名與內容。
今天 disk.c 的 disk_read() 負責讀 sector,fs_read() 負責把檔名轉成位置與長度。
把兩層分開後,我們才看得出「磁碟能讀」與「檔案找得到」分別在哪裡成立。
VirtIO 的入門路線參考《OS in 1,000 Lines》〈磁碟 I/O〉,檔案層則採本系列自訂的兩個 sector 格式。
它不是該教材檔案系統章節使用的 TAR 格式,也不是 FAT 或 ext4,不能交給一般掛載工具當成標準檔案系統。
在 30-days-os-kernel/examples/tiny-kernel/ 執行:
make DAY=18
ls -lh build/day18/disk.img
od -Ax -tx1 -N 32 build/day18/disk.img
dd if=build/day18/disk.img bs=512 skip=1 count=1 status=none | strings
最後一行只讀取專案內的映像檔,不涉及實體磁碟。
預期檔案大小是 1,024 bytes,第二個 sector 能找到 Hello from hello.txt on VirtIO disk!。
mkdisk.py 產生的格式如下:
| 位置 | 大小 | 內容 |
|---|---|---|
| Sector 0,offset 0 | 8 bytes | Magic:TINYFS1 與結尾 NUL |
| Sector 0,offset 8 | 16 bytes | NUL 結尾的 hello.txt 檔名欄位 |
| Sector 0,offset 24 | 4 bytes | 檔案長度,little-endian |
| Sector 0,offset 28 | 4 bytes | 資料 sector 編號,目前固定為 1 |
| Sector 1 | 512 bytes | 檔案內容與補齊空間 |
這個格式只容納一個唯讀檔案,沒有目錄、空間配置或寫入復原機制。
簡化後的好處是可以直接檢查每個欄位,完整看見檔案抽象如何落到區塊位置。
Day 18 的 Makefile 會在原本 RV32 啟動參數之外加入:
-global virtio-mmio.force-legacy=true \
-drive file=build/day18/disk.img,if=none,format=raw,id=labdisk \
-device virtio-blk-device,drive=labdisk,bus=virtio-mmio-bus.0
這是參數片段,實際執行請使用後面的 make DAY=18 run。
我們明確選擇 legacy VirtIO MMIO 介面,驅動也會確認 MMIO version 等於 1。
後面 xv6 的驅動使用 modern 介面,不應把兩邊的初始化暫存器混用。
disk_init() 會搜尋 QEMU virt 的 VirtIO MMIO slots,檢查 magic 與 block device ID,再設定 queue。
頁表已經映射這些 MMIO 範圍,否則核心可能在讀取裝置資訊前就先發生 page fault。
一次讀取請求使用三個串接的 descriptor:
請求 header(讀取操作、sector 編號)
→ 512-byte 接收緩衝區
→ 1-byte 完成狀態
第一項由裝置讀取,後兩項允許裝置寫入。
驅動填好 descriptor 與 available ring,通知裝置,再輪詢 used ring 是否有完成項目。
本次 queue size 為 8,但同一時間只送出一個同步請求。
共享欄位存取搭配記憶體屏障,避免只把 C 程式的敘述順序當成裝置必然看到的順序。
傳給裝置的是實體位址,目前依靠核心的 identity mapping 取得,還沒有一般化的 DMA mapping 或 IOMMU 管理。
驅動在等待迴圈中檢查完成狀態,超過迴圈上限就 panic,避免無限等待。
這個上限不是精確的毫秒 timeout,因為迴圈速度會受模擬環境影響。
我們尚未把 I/O 等待接到中斷或 Day 13 的 WAITING 狀態,CPU 在這段時間仍忙於輪詢。
這正是恐龍書中 polling 與 interrupt-driven I/O 差異的可觀察版本:先把資料讀對,再逐步改善等待方式。
Day 17 的 syscall dispatcher 新增編號 3,介面是:
| 欄位 | 約定 |
|---|---|
a0 |
使用者接收緩衝區的 VA |
a1 |
緩衝區容量 |
回傳 a0 |
成功時的 bytes 數,失敗為 −1 |
這個呼叫固定讀取 hello.txt,一次最多 128 bytes。
它還不是具有 file descriptor 與檔案偏移量的一般 read()。
User 程式在堆疊上保留緩衝區,取得內容後,再透過編號 1 逐字輸出。
核心不能直接相信 a0 是合法指標。
user.c 的 copyout() 先檢查範圍加法是否溢位,再確認整段目的範圍都映射到 U/W 頁面,全部通過後才開始複製。
實際寫入使用轉譯後 PA 的核心 identity alias,並沒有開啟 SUM 直接存取 user VA。
這份逐 byte 檢查刻意容易閱讀,但不適合大量資料搬移,也未涵蓋多核心同時修改頁表的情況。
仍在 30-days-os-kernel/examples/tiny-kernel/ 執行:
make DAY=18 run
預期在頁表訊息之後看到:
missing file and kernel-pointer rejection=ok
enter U-mode
Hello from hello.txt on VirtIO disk!
user exit=0
第一行來自兩個核心端檢查:不存在的檔案應回傳失敗,將核心位址當成使用者接收緩衝區也必須被拒絕。
它們是 helper 層的負向測試,不是已經用惡意 U-mode 程式測過所有輸入。
後面的文字則確實經過磁碟讀取與 syscall 返回路徑。
想確認輸出真的來自磁碟,可以修改 mkdisk.py 的訊息,重新執行 make DAY=18 run。
Makefile 會依腳本更新磁碟映像,使用者程式的輸出迴圈不必跟著修改。
訊息請維持在本次 128-byte 讀取容量內。
如果停在 queue 等待,先檢查 force-legacy=true、descriptor 權限與 MMIO slot。
如果是找不到檔案,先回頭檢查 header,而不是立即修改 Trap 或頁表程式。
測試後按 Ctrl+A 再按 X 離開 QEMU。
我們已有能各自重現的排程、頁表與使用者模式實驗,今天又接通了從 user request 到磁碟內容的路徑。
不過,多使用者行程、搶占式排程、一般檔案 API,以及可寫檔案系統仍未整合,這還不是可替代 xv6 的完整系統。
本日新增或擴充的檔案是 disk.c、mkdisk.py、user.c、user.S 與 Makefile 的磁碟參數。
建議 commit:
day18: read a tiny file through virtio and a user syscall
Day 19 會帶著這些具體經驗進入 xv6,看看較完整的核心如何把相同責任組合在一起。